Skip to content

Instantly share code, notes, and snippets.

@bigodel
Last active May 20, 2024 14:11
Show Gist options
  • Select an option

  • Save bigodel/15599f3c1da23d1008b7d7d4ff8bff77 to your computer and use it in GitHub Desktop.

Select an option

Save bigodel/15599f3c1da23d1008b7d7d4ff8bff77 to your computer and use it in GitHub Desktop.
Literate Ledger Programming(?) with Org-mode
#+title: Literate Ledger Programming(?) with Org-mode
#+roam_tags: org-mode ledger
Recently, given the economic hardships presented by the current pandemic,
coupled with the fact that I have started making my own money, I have started to
keep track of my finances. I have always known that I would use [[https://www.ledger-cli.org/][Ledger]], the
command line account tool, to manage my money. Ledger was created by an Emacs
maintainer, which hints to a very good Emacs integration, and indeed
[[https://www.ledger-cli.org/3.0/doc/ledger-mode.html][=ledger-mode=]] offers a great environment for editing, reporting and modifying
Ledger files. But even though it is a pretty complete major mode, I missed being
able to organize it my way. Sure you can use =outline-minor-mode= or even
[[https://github.com/alphapapa/outshine/][=outshine=]], but I wanted to have it in Org-mode.
I should also note that, from now on, I'll be referring to editing a source
block in a buffer that has all of the content of the final tangled file as
editing a source block /with context/. I actually named it
=org-contextual-src-mode=.
Keep in mind that the title says "Literate *Ledger* Programming(?)", but this is
(probably) reproducible with any language that you desire. It's just that I use
it mainly for Ledger and, at the point of writing this, have only seen real use
for Ledger.
* Why not just have it as an Org file?
Well, because there is some value in using =ledger-mode= (and, thinking about it
a little, in other modes as well) with the context of the full file when editing
it. In =ledger-mode= one can, for example, generate reports and get the balance
for a single account, as well as having completion based on your previous
postings and whatnot. But mostly I guess its because I've always wanted to be
able to edit a part of a bigger file in Org-mode, and this was the turning point
for me to take a jab at it.
I like to have Org-mode help me organize things in my life, and I am having more
and more of a tendency to do all things in Emacs and from the things I do in
Emacs, the more I can integrate with Org-mode the better. I have also wrote
about how I [[file:20210308020111-contrived_la_tex_templates_for_org_mode.org][have Org-mode manage my templates]] previously, and this is going in
sort of a similar direction.
It might be worth noting that most of the benefits from using what I describe
here can be achieved with setting the variable =ledger-master-file= on something
like =.dir-locals.el=. This variable being set tells ledger to use that file for
reports and balances, instead of the current buffer.
* The basics of the setup a.k.a. tl;dr
I'll try to be brief. I overrode =org-edit-src-code= with an =:around= advice
that tests if the current element is a source block[fn:1], which is necessary
since =org-edit-src-code= is also used for example blocks, and if the language
of the source block is in =org-contextual-src-langs= which is a variable that
holds a list of languages that should consider the context of the whole tangled
file when editing. In particular, ="ledger"= is a member of this list, so is
="latex"= in my particular case.
Then, after that's established, I tangle all source blocks related to the file
of the current block, if any (if none, just call the usual =org-edit-src-code=),
and then create a new buffer with the contents of the tangled file, but narrowed
to the result of searching the current block's content in the file. The tangling
is necessary since you might be in the middle of editing the block and haven't
tangled yet, in which case the resulting file is incomplete and the search will
fail.
** How to use it
It is pretty simple:
1. Add the file =org-contextual-src.el= to your =load-path= and call ~(require
'org-contextual-src)~.
2. Set the variable =org-contextual-src-langs= to the languages you would like
to have the context when editing (by default it is set to ~'("ledger"
"latex")~).
3. Call =org-contextual-src-mode= in an Org buffer. The minor mode is
buffer-local, so you would call it only on the buffers you would like to have
this behavior. Although, if you always want it, it can be achieved with
~(add-hook 'org-mode-hook #'org-contextual-src-mode)~.
* Let's get into details
I'll try to explain my thought process so that you could replicate what I have
achieved in your own way, if you wish to do so. I'll also be providing a [[https://gist.github.com/bigodel/15599f3c1da23d1008b7d7d4ff8bff77][gist]]
with this document in Org-mode and the Emacs Lisp file with the whole code.
As a first step, I took a look at what function is being called when issuing the
keybinding =C-c '= in a source block. Turns out it is =org-edit-special=, which
is a catch-all function to edit all sorts of blocks and structures within
Org-mode. From that, I looked through the definition to figure out the function
responsible for editing source blocks, and it so happens that it is
=org-edit-src-code=. This function takes care of editing source blocks and
example blocks, but I am only interested in the source block part.
** Wrapping =org-edit-src-code=
In my =org-contextual-edit-src= (the name I gave for the wrapper of
=org-edit-src-code=), I mimmick most of the behavior of the original function.
One minor difference is that the first =let=-bind [[(tangle)][gets the information for the
=:tangle= header argument]] and [[(file)][tries to resolve the file]]
#+caption: The new variables introduced to =org-edit-src-code=.
#+begin_src emacs-lisp
;; (let (...
(block-begin (and babel-info (nth 5 babel-info)))
(tangle (and babel-info (cdr (assq :tangle (nth 2 babel-info))))) ; (ref:tangle)
(file (cond ; (ref:file)
((string= tangle "yes")
(expand-file-name
(concat (file-name-sans-extension (buffer-name)) "."
(or (cdr (assoc lang org-babel-tangle-lang-exts))
lang))))
((string= tangle "no") nil)
((> (length tangle) 0)
(expand-file-name tangle))))
;; ...
#+end_src
The big difference comes when a few conditions are met, in order (at the time of
writing this), but not limited to:
- The argument =CODE= is nil.
This argument is only used when calling =org-babel-expand-src-block=. In this
function, the contents of the source block are expanded (noweb references,
etc.) and they are passed to =org-edit-src-code= to be edited. Since we won't
be doing any editing in this case, just call the original function.
- The current block is a *source* block.
As mentioned previously, =org-edit-src-code= also handles example blocks, but
we are only interested in source blocks.
- The language of the current block is a member of =org-contextual-src-langs=.
This variable consists of a list of strings, which in turn are Org Babel
languages, for which I would like to have contextual editing. I do so because
if in a same file I have some Emacs Lisp source blocks and some Ledger source
blocks, I wouldn't want to have contextual editing for the Emacs Lisp blocks,
but would do for the Ledger blocks, so enabling =org-contextual-src-mode=
would give me the desired behavior.
- The header argument =:noweb= is =no=.
I also don't want to edit blocks that have noweb references with their
context. I rely on searching for the block's contents in the tangled file in
order to narrow the editing buffer to the correct place, so editing a block--
with its context--that has a noweb reference, would require me to either
expand the reference, in which case the final result that should be written to
the source block is going to be the full expanded reference, which goes in the
opposite direction of the intentions of noweb references.
- If a file to be tangled has been identified.
This is done either through the header argument [[https://orgmode.org/manual/Extracting-Source-Code.html#Header-arguments][=:tangle=]] set to either =yes=
or a filename, or by giving an ID to the source block ([[https://orgmode.org/manual/Structure-of-Code-Blocks.html][=#+name=]] property or
[[https://orgmode.org/manual/Noweb-Reference-Syntax.html][=:noweb-ref=]] header argument.) I'll get into a bit more detail [[#file-from-noweb][later]].
If all of these are met, we then tangle all of the blocks for the tangle file of
the block at point.
Now comes the most relevant part.
*** Where the "magic" happens
This is the point where I need to tangle the file. We only ACTUALLY need to
tangle if the file was modified, because since we are going to use the contents
of the source block to search on the tangled file, that means that if the
current Org buffer is modified (checked with ~(buffer-modified-p)~), the
resulting tangled file is probably out of date, so update it.
#+caption: Code relevant to the tangling.
#+begin_src emacs-lisp
;; tangle only the file associated with the current block if the buffer
;; was not saved
(when (buffer-modified-p)
(save-excursion
(goto-char block-begin)
(org-babel-tangle '(16))))
#+end_src
Ok so what is really the difference between the usual approach to editing source
blocks? It's actually pretty simple. We save the value of the buffer name for
the editing buffer, which is just like the original =org-edit-src-code=, the
contents of the tangled file and the contents of the source block (note here
that the original function has an =and= to check if CODE was given, but we don't
need to do that because of the first point [[#wrapping-org-edit-src-code][above]]) in variables on a =let*=-bind.
I store them here because they are used as arguments to =org-src--edit-element=,
but I need some of them after the execution of this function, so I found it
easier to just store them on a =let= and use the values after the execution.
#+caption: Values that I need outside =org-src--edit-element=.
#+begin_src emacs-lisp
(let* ((buffer-name (or edit-buffer-name
(org-src--construct-edit-buffer-name
(buffer-name) lang)))
;; the contents of the tangled file (if we got to here, we have it)
(file-contents (with-temp-buffer
(insert-file-contents file)
(buffer-substring-no-properties
(point-min) (point-max))))
;; the actual contents of the source block.
(contents (nth 2 (org-src--contents-area elt))))
;; (org-src--edit-element ...)
#+end_src
Inside this =let=, the first thing we do is call =org-src--edit-element=. The
call is very similar to how =org-edit-src-code= does it, the only differences
being that =buffer-name= is used instead of the =or=, since we saved that in a
variable, and the contents we give this function is the contents of the tangled
file (=file-contents=).
After the execution of =org-src--edit-element=, our cursor is (supposedly) on
the newly generated edit buffer, with the appropriate name and buffer-local
variables, and the contents of the whole file; now all that is left is for us to
narrow to the contents of the block:
#+caption: The "magic".
#+begin_src emacs-lisp
;; narrow to the relevant part
(with-current-buffer buffer-name
(let* ((contents-end (search-forward contents))
(contents-beg (search-backward contents)))
(narrow-to-region contents-beg contents-end)
;; enable it in the editing buffer ; (ref:enable-mode)
(org-contextual-src-mode t)))
#+end_src
As a small side note, I [[(enable-mode)][activate the mode in the editing buffer]] because I use
the fact that this is a contextual editing buffer in other parts of it.
*** Checking for a file in other blocks
:PROPERTIES:
:CUSTOM_ID: file-from-noweb
:END:
One caveat I found when first implementing this is that if I had, for example, a
single source block with a bunch of noweb references to other blocks in other
places, if I tried to edit one of these blocks with its context, it would fail,
and that forced me to place the blocks in the order in which I wished they would
appear in the file. But, since this is Emacs and we're dealing with Emacs Lisp,
of course there is a workaround.
Before[fn:2] checking [[#wrapping-org-edit-src-code][these points]], I check if the file is null, e.g., I
couldn't figure out the file to be tangled from current source block. When that
is the case, I have a function that [[(name-or-ref)][gets the name or noweb reference]] for the
current block, and searchs for =<<name-or-ref>>=. For each match, check if the
we are currently on a source block (it could be an internal link) and if so,
[[(file-noweb)][extract the file to be tangled]]. Yeah, this function retrieves the file from the
*first* match of a source block, but I couldn't figure out how to do it in
general, and this seems like it would cover 99.5% of the cases.
#+caption: Getting the file from a noweb reference.
#+begin_src emacs-lisp
(let* (;; ...
(name-or-ref (or (and (not (string= "" name)) name) ; (ref:name-or-ref)
;; or it has a noweb-ref (name has precedence)
(cdr (assq :noweb-ref (nth 2 babel-info)))))
;; ...
)
;; ...
(when (not (string= "no" match-tangle))
(setq file (expand-file-name ; (ref:file-noweb)
(cond
((string= match-tangle "yes")
(concat
(file-name-sans-extension (buffer-name)) "."
(or (cdr (assoc lang org-babel-tangle-lang-exts))
lang)))
((> (length match-tangle) 0) match-tangle))
block-begin (nth 5 babel-info))))
(list file block-begin))
#+end_src
** A few modifications on other functions
Some other functions had to be advised for it to work. One of those functions
was =org-edit-src-exit=. This function is what is usually bound to =C-c '= in
the dedicated editing buffer, and is responsible for finishing the editing
session, while writing the edited content back on the source block in the Org
file. It doesn't differ much from the original function, but we have to add an
advice arround it, so we need to check when to use our modified version, and
when to use the original functoh, like so
<<advice-check>>
#+caption: Function to be wrapped around =org-edit-src-exit=.
#+begin_src emacs-lisp
(defun org-contextual-src-edit-exit (orig-fun &rest args)
(if (or org-contextual-src--no-context-p
(not org-contextual-src-mode))
(apply orig-fun args)
;; ...
#+end_src
I only had to change two little things on my custom function: 1. the variable
=coordinates= is populated with =org-contextual-src--coordinates=, which gets
the coordinates in which to put the cursor at the Org buffer, in relation to the
a beginning and end position, which are, in the original =org-edit-src-exit=, 1
as the beginning position, which corresponds to the beginning of the buffer,
regardless of narrowing, and =(point-max)=, which is the position of the end of
the buffer; however, in our version we would like to consider only the text
inside the narrowing, so we simply substitute the 1 for =(point-min)=, and the
=org-contextual-src--coordinates= behaves just like =org-src--coordinates= but
without wrapping it with a =org-with-wide-buffer=; and 2. set the variable
=org-contextual-src--no-context-p= to =nil=. More about this variable [[before-advice][below]].
Another one of those functions was =org-src--contents-for-write-back=, which is
a function that returns the contents that should be written back to the block.
This function had to be advised because it was getting the contents of the whole
buffer, unwidened, which would mean that the whole final file would be written
back to the block. That is obviously not what we want, we want to get the
contents of the *visible* part of the buffer, since we have it already narrowed.
I also had to check if we are using the context or not just like [[advice-check][here]]. The rest
of the function is exactly like the original =org-src--contents-for-write-back=,
with the exception that the variable =contents= is initialized with
~(buffer-string)~ instead of ~(org-with-wide-buffer (buffer-string))~, similarly
to =org-contextual-src--coordinates=. It is used when either saving or exiting,
that's why we couldn't change the behavior of it only in the case that we are
calling =org-contextual-src-edit-exit= in a buffer that is considering the
context, like we did with =org-contextual-src--coordinates=.
<<before-advice>>
There is only one other function which I have advised, but this one is much
simpler. When you issue a command inside a source block, Org actually executes
it in a dedicated edit buffer, just like if you have called =org-edit-src-code=,
but invisibly. Because of that, we have to override this behavior in case we are
on a block that should be executed with the context. That is because, if
everytime we issue a command in a source block we have to actually tangle it,
given that we are going to call our =org-contextual-edit-src=, which tangles all
blocks related to the final file, it would be really annoying to edit any source
block in the Org file itself, but sometimes for quick edits its just easier to
change it right there. The function that does this,
=org-babel-do-key-sequence-in-edit-buffer=, has a =:before= advice that sets
=org-contextual-src--no-context-p= to =t=, that's why all of the functions that
are advices check if the mode is on, or if this variable is true.
** Defining the minor mode
In order to make it easier to activate this behavior on specific buffers, I
gathered the advicing of the functions in a buffer-local minor mode. The body of
the minor mode is:
#+begin_src emacs-lisp
(if org-contextual-src-mode
(progn
(advice-add 'org-edit-src-code :around #'org-contextual-src-edit-code)
(advice-add 'org-edit-src-exit :around #'org-contextual-src-edit-exit)
(advice-add 'org-src--contents-for-write-back
:around #'org-contextual-src--contents-for-write-back)
(advice-add 'org-babel-do-key-sequence-in-edit-buffer
:before
#'org-contextual-src-do-key-sequence-in-edit-buffer-advice)
(dolist (func org-contextual-src--narrow-functions) ; (ref:disable-narrow)
(advice-add `,func :around #'org-contextual-src-narrow-advice)))
(advice-remove 'org-edit-src-code #'org-contextual-edit-src)
(advice-remove 'org-edit-src-exit #'org-contextual-edit-src-exit)
(advice-remove 'org-src--contents-for-write-back
#'org-src--contextual-contents-for-write-back)
(advice-remove 'org-babel-do-key-sequence-in-edit-buffer
#'org-babel-do-key-sequence-in-edit-buffer-ad)
(dolist (func org-contextual-src--narrow-functions)
(advice-add `,func :around #'org-contextual-src-narrow-advice)))
#+end_src
<<disable-narrowing>>
I guess I should mention that I also [[(disable-narrow)][disable any narrowing function]] inside the
contextual editing buffer, because that would mess up how we get the contents to
be written in the block with our =org-contextual-src--contents-for-write-back=.
And done!
* Conclusion and caveats
And this is basically it. I know it might have been a lot to digest, and I
recognize that I may be wordy most of the time, but I wanted to share this
little hack with the world because I feel that many would like to keep their
=ledger= journal in Org-mode but not being able to access the context of the
whole file have turned them away.
There are some caveats with it, though. The biggest one is that if these
functions ever get changed I would have to reflect the change and adapt the
modified code to work as I have intended, though I don't think this would be
much of a problem since this part of Org-mode seems stable enough. Another one
of those is that one cannot call any of the narrowing functions, as mentioned
[[disable-narrowing][above]]. Another one is that it takes a couple of milliseconds before the editing
buffer shows up because of the tangling. But despite them, I am enjoying very
much having sort of "the best of both worlds."
One minor issue that I haven't yet solved, and don't know what is causing it, is
that calling =save-buffer= saves the entire file to the source block, instead of
just the visible contents of the buffer. In the editing buffer, =C-x C-s= gets
bound to =org-edit-src-save=, which works as desired without having to advise or
anything like that (because we've already advised
=org-src--contents-for-write-back=), but calling =save-buffer= behaves like I
mentioned, which might not be ideal.
Ps.: I know that one shouldn't [[https://karl-voit.at/2020/10/23/avoid-web-forums/][contribute anything relevant in web forums like
Reddit]], but I haven't had the time to setup my blog yet so I'm just posting here
for now.
* Footnotes
[fn:2] I check it before checking the points becuase one of the checks I didn't
mention is if the file is null, in which case no tangling is happening and we
should just use the original function.
[fn:1] I don't think it would be beneficial to have it working with inline
source blocks, but that can be arranged.
;;; org-contextual-src.el --- Edit SRC block with context in Org-mode -*- lexical-bindings: t; -*-
;; Author: João Pedro de Amorim Paula <jpedrodeamorim@gmail.com>
;;; Commentary:
;;
;; This is basically a hack around `org-edit-src-code' that tangles all the
;; blocks for the tangle file of the block at point and then proceeds to create
;; a buffer with a copy of the tangled file so that we can edit this buffer,
;; instead of the real file, narrowed to the block we were interested in. this
;; allows us to have all of the context of the final file, while still editing
;; it as it were just a simple code block in org-mode.
;;
;;; Code:
(require 'ob-tangle)
(require 'org-element)
(require 'org-src)
(defvar org-contextual-src-langs '("ledger" "latex")
"List of Babel languages that should be edited with the context.")
(defvar org-contextual-src--no-context-p nil
"Variable to store if we should use or not the context.")
;; silence byte-compiler
(defvar org-contextual-src-mode)
(defun org-contextual-src-edit-code (orig-fun &optional code edit-buffer-name)
"Edit the source block at point with tangle context.
This is meant to be used as an :around advice on
`org-edit-src-code', so ORIG-FUN is precisely the symbol for the
function, and CODE and EDIT-BUFFER-NAME are the arguments for
`org-edit-src-code'.
If the block has a NAME property or :noweb-ref header arguments,
and has no :tangle header argument, i.e., couldn't determine the
file to be tangled, try to look for a source block that tangles
the current block and tangle the first block that matches the
search and which :tangle is anything other than 'no'."
(interactive)
(let* ((elt (org-element-at-point))
(type (car elt))
(lang (when (eq type 'src-block)
(org-element-property :language elt)))
(lang-f (and (eq type 'src-block) (org-src-get-lang-mode lang)))
(babel-info (and (eq type 'src-block)
(org-babel-get-src-block-info 'light)))
(block-begin (and babel-info (nth 5 babel-info)))
(tangle (and babel-info (cdr (assq :tangle (nth 2 babel-info)))))
(file (cond
((string= tangle "yes")
(expand-file-name
(concat (file-name-sans-extension (buffer-name)) "."
(or (cdr (assoc lang org-babel-tangle-lang-exts))
lang))))
((string= tangle "no") nil)
((> (length tangle) 0)
(expand-file-name tangle)))))
;; only try to follow a noweb reference if we couldn't figure out the
;; file to be tangled
(unless file
(let ((file-alist (org-contextual-src--file-from-noweb babel-info)))
(setq file (car file-alist)
block-begin (cadr file-alist))))
;; don't edit with context if any of these conditions are met
(if (or org-contextual-src--no-context-p
(not org-contextual-src-mode) ; the mode is not on
code ; if CODE is not nil, don't bother
(not (eq type 'src-block))
(not (member lang org-contextual-src-langs))
;; doesn't really work with noweb, so this return t if :noweb is
;; not nil or is something other than
(not (string= "no" (cdr (assq :noweb (nth 2 babel-info)))))
(null file))
(apply orig-fun `(,code ,edit-buffer-name))
;; tangle only the file associated with the current block if the buffer
;; was not saved
(when (buffer-modified-p)
(save-excursion
(goto-char block-begin)
(org-babel-tangle '(16))))
;; store some values we want to use after
(let* ((buffer-name (or edit-buffer-name
(org-src--construct-edit-buffer-name
(buffer-name) lang)))
;; the contents of the tangled file (if we got to here, we have it)
(file-contents (with-temp-buffer
(insert-file-contents file)
(buffer-substring-no-properties
(point-min) (point-max))))
;; the actual contents of the source block.
(contents (nth 2 (org-src--contents-area elt))))
;; open the edit buffer
(org-src--edit-element elt buffer-name lang-f
(and (null code)
(lambda () (org-escape-code-in-region
(point-min) (point-max))))
;; the contents of the buffer should be the
;; contents of the file
file-contents)
;; narrow to the relevant part
(with-current-buffer buffer-name
(let* ((contents-end (search-forward contents))
(contents-beg (search-backward contents)))
(narrow-to-region contents-beg contents-end)
;; enable it in the editing buffer
(org-contextual-src-mode t))))
;; finalize buffer.
(setq-local org-coderef-label-format
(or (org-element-property :label-fmt elt)
org-coderef-label-format))
(when (eq type 'src-block)
(setq org-src--babel-info babel-info)
(let ((edit-prep-func (intern (concat "org-babel-edit-prep:" lang))))
(when (fboundp edit-prep-func)
(funcall edit-prep-func babel-info))))
t)))
(defun org-contextual-src--file-from-noweb (babel-info)
"Get the tangle file for current block.
Search for the first match of the noweb reference, which is
either the value of the header argument :noweb-ref or the NAME
for the current block, and return the file it is supposed to
tangle to. BABEL-INFO is the result of
`org-babel-get-src-block-info'.
The return value of this function is an alist whose `car' is the
file name and `cdr' is the position of the first match."
(let* ((lang (nth 0 babel-info))
(name (nth 4 babel-info))
;; block has a name so return it
(name-or-ref (or (and (not (string= "" name)) name)
;; or it has a noweb-ref (name has precedence)
(cdr (assq :noweb-ref (nth 2 babel-info)))))
file block-begin match-pos match-info match-tangle)
(when name-or-ref
(save-excursion
(goto-char (point-min))
(while (null file)
(setq match-pos (search-forward
(org-babel-noweb-wrap name-or-ref) nil 'noerror)
match-info (and (eq (car (org-element-at-point)) 'src-block)
(org-babel-get-src-block-info 'light))
match-tangle (cdr (assq :tangle (nth 2 match-info))))
;; we found a use of the reference of the block, now we need
;; to check if it is supposed to tangle or not
(when (not (string= "no" match-tangle))
(setq file (expand-file-name
(cond
((string= match-tangle "yes")
(concat
(file-name-sans-extension (buffer-name)) "."
(or (cdr (assoc lang org-babel-tangle-lang-exts))
lang)))
((> (length match-tangle) 0) match-tangle)))
block-begin (nth 5 match-info))))))
(list file block-begin)))
(defun org-contextual-src--contents-for-write-back (orig-fun &rest args)
"Like `org-src--contents-for-write-back' but be context aware.
This is meant to be used as an :around advice on
`org-src--contents-for-write-back', so that's why ORIG-FUN and
ARGS are its arguments.
The only difference compared to
`org-src--contents-for-write-back' is that the variable
'contents' is just `buffer-string', without
`org-with-wide-buffer'."
(interactive)
(if (or org-contextual-src--no-context-p
(not org-contextual-src-mode))
(apply orig-fun args)
(let ((indentation-offset
(if org-src--preserve-indentation 0
(+ (or org-src--block-indentation 0)
(if (equal org-src--source-type 'src-block)
org-src--content-indentation
0))))
(use-tabs? (and (> org-src--tab-width 0) t))
(source-tab-width org-src--tab-width)
(contents (buffer-string))
(write-back org-src--allow-write-back))
(with-temp-buffer
;; reproduce indentation parameters from source buffer.
(setq indent-tabs-mode use-tabs?)
(when (> source-tab-width 0) (setq tab-width source-tab-width))
;; apply WRITE-BACK function on edit buffer contents.
(insert (org-no-properties contents))
;; we sometimes need to add 1 newline because we have a part of
;; buffer, and not a complete buffer with the newline that Emacs adds
(unless (and (goto-char (point-max)) (bolp) (eolp))
(newline 1))
(goto-char (point-min))
(when (functionp write-back) (save-excursion (funcall write-back)))
;; add INDENTATION-OFFSET to every non-empty line in buffer,
;; unless indentation is meant to be preserved.
(when (> indentation-offset 0)
(while (not (eobp))
(skip-chars-forward " \t")
(unless (eolp) ;ignore blank lines
(let ((i (current-column)))
(delete-region (line-beginning-position) (point))
(indent-to (+ i indentation-offset))))
(forward-line)))
(buffer-string)))))
(defun org-contextual-src--coordinates (pos beg end)
"Like `org-src--coordinates' but don't widen.
POS, BEG and END are the same as in `org-src--coordinates'."
(if (>= pos end) 'end
(goto-char (max beg pos))
(cons (count-lines beg (line-beginning-position))
;; column is relative to the end of line to avoid problems of
;; comma escaping or colons appended in front of the line.
(- (current-column)
(progn (end-of-line) (current-column))))))
(defun org-contextual-src-edit-exit (orig-fun &rest args)
"Like `org-edit-src-exit' but context aware.
This is meant to be used as an :around advice on
`org-edit-src-exit', so that's why ORIG-FUN and ARGS are its
arguments.
The only difference compared to `org-edit-src-exit', is that the
variable 'coordinates' is the value of calling
`org-contextual-src--coordinates', instead of
`org-src--coordinates', with `point-min' instead of 1."
(interactive)
(if (or org-contextual-src--no-context-p
(not org-contextual-src-mode))
(apply orig-fun args)
(unless (org-src-edit-buffer-p) (error "Not in a sub-editing buffer"))
(let* ((beg org-src--beg-marker)
(end org-src--end-marker)
(write-back org-src--allow-write-back)
(remote org-src--remote)
(coordinates (and (not remote)
(org-contextual-src--coordinates
(point) (point-min) (point-max))))
(code (and write-back (org-src--contents-for-write-back))))
(set-buffer-modified-p nil)
;; switch to source buffer. kill sub-editing buffer.
(let ((edit-buffer (current-buffer))
(source-buffer (marker-buffer beg)))
(unless source-buffer (error "Source buffer disappeared. Aborting"))
(org-src-switch-to-buffer source-buffer 'exit)
(kill-buffer edit-buffer))
;; insert modified code. ensure it ends with a newline character.
(org-with-wide-buffer
(when (and write-back (not (equal (buffer-substring beg end) code)))
(undo-boundary)
(delete-region beg end)
(let ((expecting-bol (bolp)))
(insert code)
(when (and expecting-bol (not (bolp))) (insert "\n")))))
;; if we are to return to source buffer, put point at an
;; appropriate location. in particular, if block is hidden, move
;; to the beginning of the block opening line.
(unless remote
(goto-char beg)
(cond
;; block is hidden; move at start of block.
((cl-some (lambda (o) (eq (overlay-get o 'invisible) 'org-hide-block))
(overlays-at (point)))
(beginning-of-line 0))
(write-back (org-src--goto-coordinates coordinates beg end))))
;; clean up left-over markers and restore window configuration.
(set-marker beg nil)
(set-marker end nil)
(when org-src--saved-temp-window-config
(unwind-protect
(set-window-configuration org-src--saved-temp-window-config)
(setq org-src--saved-temp-window-config nil)))))
;; reset the value of this variable regardless of contextual editing or not
(setq org-contextual-src--no-context-p nil))
(defun org-contextual-src-do-key-sequence-in-edit-buffer-advice (&rest _)
"Set `org-contextual-src--no-context-p' to t.
This is meant to be used as a :before advice on
`org-babel-do-key-sequence-in-edit-buffer'. If this function was
called, that means that we don't want to execute the key sequence
in an edit buffer, since that would imply that we would tangle
and all of that."
(setq org-contextual-src--no-context-p t))
(defun org-contextual-src-narrow-advice (orig-fun &rest args)
"Function to be used as :around advice on narrowing functions.
This function is supposed to wrap `narrow-to-region',
`narrow-to-defun', `narrow-to-page' and `widen'. This is supposed
to check if the current buffer is a source editing buffer and if
`org-contextual-src-mode' is non-nil. On the contrary, just call
the usual function. ORIG-FUN and ARGS are what is necessary to
call the original function."
(if (and (fboundp 'org-src-edit-buffer-p)
(org-src-edit-buffer-p)
org-contextual-src-mode)
(user-error "Narrowing or widening not allowed in contextual editing")
(apply orig-fun args)))
(defvar org-contextual-src--narrow-functions
'(narrow-to-defun narrow-to-page narrow-to-region widen)
"Narrowing functions that should be advised.
Symbols in this list are expected to be functions and won't work
on editing buffer for contextual editing.")
;;;###autoload
(define-minor-mode org-contextual-src-mode
"Edit Org-mode files in a separate buffer with all tangled blocks.
What this means, is that if you're editing a source block in
Org-mode that is going to be tangled, when using
`org-edit-special', if the block's language is in
`org-contextual-src-langs', then the editing buffer is actually
the whole tangled file narrowed, using `narrow-to-region', to the
relevant part."
:global nil
:lighter " OrgCtxSrc"
(if org-contextual-src-mode
(progn
(advice-add 'org-edit-src-code :around #'org-contextual-src-edit-code)
(advice-add 'org-edit-src-exit :around #'org-contextual-src-edit-exit)
(advice-add 'org-src--contents-for-write-back
:around #'org-contextual-src--contents-for-write-back)
(advice-add 'org-babel-do-key-sequence-in-edit-buffer
:before
#'org-contextual-src-do-key-sequence-in-edit-buffer-advice)
(dolist (func org-contextual-src--narrow-functions)
(advice-add `,func :around #'org-contextual-src-narrow-advice)))
(advice-remove 'org-edit-src-code #'org-contextual-edit-src)
(advice-remove 'org-edit-src-exit #'org-contextual-edit-src-exit)
(advice-remove 'org-src--contents-for-write-back
#'org-src--contextual-contents-for-write-back)
(advice-remove 'org-babel-do-key-sequence-in-edit-buffer
#'org-babel-do-key-sequence-in-edit-buffer-ad)
(dolist (func org-contextual-src--narrow-functions)
(advice-add `,func :around #'org-contextual-src-narrow-advice))))
(provide 'org-contextual-src)
;;; org-contextual-src.el ends here
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment